The ASP.NET platform originally developed around the idea of serving requests for physical pages. This approach has worked for a few years, and still works today in the context of ASP.NET Web Forms. This was a useful pattern when web sites were created from individual file-based pages linked together to form a sitemap. Today, we still have such pages, but we also have to create pages by aggregating data from different endpoints to both the client and the server.
This led to a new requirement; HTTP-based endpoints that are capable of serving plain data which is mostly serialized to JSON strings. This, in turn, required more control over the format of the URL. The ASP.NET run-time environment has always provided a specific technology to let you call into resources identified by a specific URL. By writing an ad hoc HTTP handler, and binding it to a URL, you can use ASP.NET to merely execute code in response to a request rather than to serve physical files.
ASP.NET MVC is built on top of these two features-HTTP handlers and ad hoc URLs. More specifically, ASP.NET MVC encapsulates HTTP handlers and ad hoc URLs in its own framework and exposes to developers a new API that is centered on controllers and routes.
On the Way to Controllers
What happens exactly when a user’s request knocks on the door of IIS (Internet Information Service) ? As Figure 1 shows, any requests pass through the list of registered modules. One of the registered modules is the URL Routing module.
FIGURE 1. The URL Routing HTTP module in IIS
Figure 1 shows the registered modules in the order in which they are processed by IIS. The URL Routing module is towards the end of the chain. The URL routing module intercepts any requests for the application that could not be otherwise served by IIS. If the URL refers to a physical file (i.e., an ASPX file), the routing module ignores the request, unless otherwise configured. The request then falls down to the classic ASP.NET machinery to be processed as usual in terms of a page handler.
Otherwise, the URL routing module attempts to match the URL of the request to any of the application defined routes. If a match is found, the request goes into the ASP.NET MVC space. If no match is found then the request is likely to result in a HTTP 404.
The URL Routing module consumes information exposed by each ASP.NET MVC application-the list of URL patterns that the application recognizes and is ready to process. Each URL pattern is associated with a route within the application. A route is a way to tie together a URL pattern with a controller action. The routing module doesn’t do the whole work internally, however. It figures out whether the request is directed at one of the ASP.NET MVC applications and then directs it to a specific HTTP handler that is part of the ASP.NET MVC framework. This handler will then resolve the request in terms of an action on a controller class. Figure 2 is an overview of request processing in ASP.NET MVC.
FIGURE 2. Request processing in ASP.NET MVC
ASP.NET MVC Routes
In ASP.NET MVC, a route is the combination of a pattern-matching string that represents a recognized URL and a controller/action pair. All ASP.NET MVC applications that you create through the Visual Studio project template are equipped with at least the following default route:
1 2 3 4 5 6 7 8 |
routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }); |
The default route is named “Default” in the first parameter. The default route catches any URLs that contains at most three segments. The first segment is assumed to be the name of the controller; the second segment is assumed to be the name of the action. The third segment is optional and if present is assumed to be the value for a parameter named Id. When the URL matches the pattern for the route, the ASP.NET MVC HTTP handler will process the URL to extract the name of the controller and action. No ASP.NET MVC application can run without routes.
A URL pattern-matching string may be ambiguous or unambiguous. In other words, it can be a string constant such as ‘/tv/channels’ or it can be a string expression such as ‘/tv/{channelId}’.
In the first example value, ‘/tv/channels’, there’s just one URL that matches the route. The controller and the action to execute are those specified as the third parameter of the MapRoute method:
1 2 3 4 5 6 7 8 |
routes.MapRoute( "Default", "/tv/channels", new { controller = "Tv", action = "Channels", id = UrlParameter.Optional }); |
Most of the time, however, you deal with dynamic routes that incorporate one or more parameters. In the second example value, ‘/tv/{channelId}’ the route is matched by any URLs that contains “tv” followed by a token. Actual examples could be:
1 2 |
/tv/1/ tv/bbc1 |
The first token, ‘tv’, is considered to be a constant; the following token is taken to be a variable. A route parameter is a name enclosed in curly brackets { }. You can have multiple parameters in a route as long as they are separated by a constant or delimiter. The forward slash (/) character acts as a delimiter between the various parts of the route. The name of the parameter is the text that you indicate between brackets. In the routes that we considered so far, the controller that is used to resolve the request is indicated as a constant in the third parameter of the call to the MapRoute method.
ASP.NET MVC requires that, for each request, the application sets a value for two predefined parameters, the controller and action parameters. This can be done declaratively as well as programmatically. You do it declaratively by using the MapRoute method and/or using the controller and action parameters in the URL. The most common way of doing this is through the default route for an ASP.NET MVC application:
1 |
{controller}/{action}/{id} |
The route contains three placeholders separated by the delimiter. A URL that matches the preceding route is the following:
1 |
/Customer/Edit/ALFKI |
The controller parameter is matched to the string “Customer”; the action parameter is matched to the string “Edit”. Finally, the id parameter is matched to the string ALFKI. Subsequently, the ASP.NET MVC framework resolves the request executing the Edit method on the Customer controller passing the ALFKI value to the id argument. If the application doesn’t include a Customer Controller class with a method Editthat accepts an id argument then you will experience an exception.
The matching process is more sophisticated than it may seem at first. For example, the following URLs are matched too:
1 2 3 |
/ /Customer /Customer/Edit |
The first lacks all three parameters; the second only has the controller and action; the third lacks the ID parameter. Missing parameters can be filled with default values that you indicate as the last parameter of the MapRoute method.
Whilst the default route works, by itself, for most applications, you can add as many routes as you wish to your ASP.NET MVC application with as many parameters as you reckon appropriate. You can even remove the default route.
Defining Routes
An application should define its supported routes during the startup process. You typically add routes to the RouteTable.Routes system collection in the Application_Start handler in global.asax. A route is an instance of the Route class that you typically, but not necessarily, create through the handy MapRoute method.
The MapRoute method offers a variety of overloads and works well most of the time. It doesn’t, however, let you configure every possible aspect of a route object. If there’s something that you need to set on a route that MapRoute doesn’t support, then you might want to resort to the following code:
1 2 |
var route = new Route(...); RouteTable.Routes.Add("YourRoute", route); |
You first create a new Route object setting all of the properties that the object offers and that you need to set. When done, you just add the Route object to the system Routes collection.
A route is not simply characterized by name, URL pattern and default values that you usually set via the MapRoute helper. A route may also have constraints, data tokens, and a custom route handler: This makes for less frequent but more interesting use cases. Let’s expand on this then.
Route Constraints
When you define a route parameter, you first focus on the name and the position of it within the URL string. The next concern, though, is about the type of data. Let’s consider the two examples below:
1 2 |
/tv/show/1 /tv/show/bbc1 |
Both are valid URLs for the following route: tv/{channelId}. This route postulates the availability of a controller like this one:
1 2 3 4 5 |
public class TvController { public ActionResult Show(int channelId) { ... } } |
What data type you expect the channel ID to be? Should it be an integer, a string or possibly both? With the code above, if the URL matches the channelId parameter to a string then the controller code is likely to throw an exception when a string like “bbc1” is cast to an integer. In situations like this, you might want to consider adding a constraint to the route. Here’s how:
1 2 3 4 5 6 |
routes.MapRoute( "Tv", "Tv/{channelId}", new {controller="Tv", action="Show"}, new {channelId = @"\d+"} ); |
The channelId parameter is bound to a regular expression that selects only digits. In this way, a URL in which the token following “Tv” is a string just doesn’t match.
If a regular expression is not enough for the logic you want to express, you can always create a route constraint class. A route constraint class is a class that implements theIRouteConstraint interface:
1 2 3 4 5 6 7 8 9 10 11 |
public class NumericRouteConstraint : IRouteConstraint { bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { ... } } |
The interface has just one method, the Match method, which returns a Boolean to indicate whether the URL matches or not. You register a route constraint class as below:
1 2 3 4 5 6 |
routes.MapRoute( "Tv", "Tv/{channelId}", new {controller="Tv", action="Show"}, new {channelId = new NumericRouteConstraint()} ); |
The route constraint class receives a lot of parameters and gives you the chance of examining nearly every aspect of the request.
Route Data Tokens
Sometimes you want to associate with a given route some additional values that are not used to determine whether a route matches a pattern. You can do this by using the DataTokensdictionary.
1 2 3 |
var route = new Route(...); route.DataTokens = new RouteValueDictionary { { "format", "short" } }; RouteTable.Routes.Add("YourRoute", route); |
Values in theDataTokens dictionary are passed to the route handler and can be used to make decisions within the route handler as to the controller or action to employ.
Custom Route Handlers
The route handler is the object that processes any requests that match a given route. The sole purpose of a route handler is to return the HTTP handler that will actually serve any matching request. Within the boundaries of a route handler, though, you can programmatically decide about the controller that will serve the request. If you need to determine from run time conditions which controller, or method, should be in charge of a given request, then a route handler is the way to go.
A route handler is a class that implements theIRouteHandler interface. The interface is defined as below:
1 2 3 4 |
public interface IRouteHandler { IHttpHandler GetHttpHandler(RequestContext requestContext); } |
The ASP.NET MVC framework doesn’t include that many route handlers, and I have never needed to create an additional one for a production web site. Yet, the extensibility point exists for you to take advantage of it in case you need to do so.
Preventing Routing for Certain URLs
The ASP.NET URL routing module doesn’t limit you to maintain a white-list of acceptable URL patterns. It also allows you to keep certain URLs off the routing mechanism. You can prevent the routing system from handling certain URLs doing as below:
1 2 3 4 5 |
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd"); ... } |
IgnoreRoute is a method on the RouteTable.Routes object; all it does is associate a special route handler-the StopRoutingHandler class-to a given route. All URLs matching the rule will then be ignored by ASP.NET MVC.
Summary
Routing is a powerful mechanism that plays a key role in modern ASP.NET development. By modern ASP.NET development I mean ASP.NET development that goes beyond the plain mapping of URLs to physical server pages. Born as part of the ASP.NET MVC infrastructure, routing is now associated with ASP.NET Web Forms and Web API. Any ASP.NET developer can’t ignore it.
Load comments